//SPDX-FileCopyrightText: Copyright 2022-2024 深圳市同心圆网络有限公司
//SPDX-License-Identifier: GPL-3.0-only

use git2::{BranchType, Error, Oid, Repository, Signature};
use serde::Serialize;
use std::{
    collections::{HashMap, HashSet},
    path::PathBuf,
};

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitUserInfo {
    name: String,
    email: String,
    timestamp: i64,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CommitInfo {
    refs: Vec<String>,
    hash: String,
    hash_abbrev: String,
    tree: String,
    tree_abbrev: String,
    parents: Vec<String>,
    parents_abbrev: Vec<String>,
    author: GitUserInfo,
    committer: GitUserInfo,
    subject: String,
    body: String,
    notes: String,
    stats: Vec<String>, //特殊处理，不计算，太消耗计算时间
    timestamp: i64, //排序用
}

pub fn exec(git_path: PathBuf, commit_id: &String) -> Result<(), Error> {
    let repo = Repository::open(git_path);
    if repo.is_err() {
        return Err(repo.err().unwrap());
    }
    let repo = repo.unwrap();

    let commit_id = Oid::from_str(commit_id);
    if commit_id.is_err() {
        return Err(commit_id.err().unwrap());
    }
    let commit_id = commit_id.unwrap();

    let mut info_list: Vec<CommitInfo> = Vec::new();
    let mut commit_id_set: HashSet<Oid> = HashSet::new();

    let res = calc_commit_info(&repo, &commit_id, &mut info_list, &mut commit_id_set);
    if res.is_err() {
        return res;
    }
    let res = fix_refs(&repo, &mut info_list);
    if res.is_err() {
        return res;
    }
    info_list.sort_by(|a,b|{b.timestamp.cmp(&a.timestamp)});
    println!("{}", serde_json::to_string(&info_list).unwrap());

    Ok(())
}

fn fix_refs(repo: &Repository, info_list: &mut Vec<CommitInfo>) -> Result<(), Error> {
    let mut ref_map: HashMap<String, Vec<String>> = HashMap::new();

    let branchs = repo.branches(Some(BranchType::Local));
    if branchs.is_err() {
        return Err(branchs.err().unwrap());
    }
    let branchs = branchs.unwrap();
    for branch in branchs {
        if branch.is_err() {
            continue;
        }
        let branch = branch.unwrap();
        let branch_oid = branch.0.get().target();
        let branch_name = branch.0.name();
        if branch_oid.is_some() && branch_name.is_ok() {
            let branch_oid = branch_oid.unwrap();
            let branch_name = branch_name.unwrap().unwrap_or_default();
            let key = branch_oid.to_string();
            let old_value = ref_map.get(&key);
            let mut old_value = if old_value.is_some() {
                old_value.unwrap().clone()
            } else {
                Vec::new()
            };
            old_value.push(branch_name.into());
            ref_map.insert(key, old_value);
        }
    }
    let res = repo.tag_foreach(|oid, name| {
        let obj = repo.find_object(oid, None);
        if obj.is_err() {
            return true;
        }
        let obj = obj.unwrap();
        let commit = obj.peel_to_commit();
        if commit.is_err() {
            return true;
        }
        let commit = commit.unwrap();
        let key = commit.id().to_string();
        let old_value = ref_map.get(&key);
        let mut old_value = if old_value.is_some() {
            old_value.unwrap().clone()
        } else {
            Vec::new()
        };
        old_value.push(format!(
            "tag: {}",
            String::from_utf8_lossy(name).to_string()
        ));
        ref_map.insert(key, old_value);
        return true;
    });
    if res.is_err() {
        return Err(res.err().unwrap());
    }

    for info in info_list {
        let refs = ref_map.get(&info.hash);
        let refs = if refs.is_some() {
            refs.unwrap().clone()
        } else {
            Vec::new()
        };
        info.refs = refs;
    }
    Ok(())
}

fn calc_commit_info(
    repo: &Repository,
    commit_id: &Oid,
    info_list: &mut Vec<CommitInfo>,
    commit_id_set: &mut HashSet<Oid>,
) -> Result<(), Error> {
    if commit_id_set.contains(commit_id) {
        return Ok(());
    }
    commit_id_set.insert(commit_id.clone());
    let info = gen_commit_info(repo, commit_id);
    if info.is_err() {
        return Err(info.err().unwrap());
    }
    let info = info.unwrap();
    let parents = info.parents.clone();
    info_list.push(info);

    for parent_commit_id in parents {
        let parent_commit_id = Oid::from_str(&parent_commit_id);
        if parent_commit_id.is_err() {
            return Err(parent_commit_id.err().unwrap());
        }
        let parent_commit_id = parent_commit_id.unwrap();
        let res = calc_commit_info(repo, &parent_commit_id, info_list, commit_id_set);
        if res.is_err() {
            return res;
        }
    }
    Ok(())
}

fn substr(content: String, start: usize, end: usize) -> String {
    return content.chars().skip(start).take(end - start).collect();
}

fn convert_git_user(sig: Signature) -> GitUserInfo {
    return GitUserInfo {
        name: sig.name().unwrap_or_default().into(),
        email: sig.email().unwrap_or_default().into(),
        timestamp: sig.when().seconds() * 1000,
    };
}

fn gen_commit_info(repo: &Repository, commit_id: &Oid) -> Result<CommitInfo, Error> {
    let mut info = CommitInfo {
        refs: Vec::new(),
        hash: commit_id.to_string(),
        hash_abbrev: substr(commit_id.to_string(), 0, 7),
        tree: "".into(),
        tree_abbrev: "".into(),
        parents: Vec::new(),
        parents_abbrev: Vec::new(),
        author: GitUserInfo {
            name: "".into(),
            email: "".into(),
            timestamp: 0,
        },
        committer: GitUserInfo {
            name: "".into(),
            email: "".into(),
            timestamp: 0,
        },
        subject: "".into(),
        body: "".into(),
        notes: "".into(),
        stats: Vec::new(),
        timestamp: 0,
    };

    let commit = repo.find_commit(commit_id.clone());
    if commit.is_err() {
        return Err(commit.err().unwrap());
    }

    let commit = commit.unwrap();
    info.timestamp = commit.time().seconds() * 1000;
    let tree_id = commit.tree_id();
    info.tree = tree_id.to_string();
    info.tree_abbrev = substr(tree_id.to_string(), 0, 7);

    let mut parents = commit.parent_ids();
    while let Some(parent_commit_id) = parents.next() {
        info.parents.push(parent_commit_id.to_string());
        info.parents_abbrev
            .push(substr(parent_commit_id.to_string(), 0, 7));
    }
    info.author = convert_git_user(commit.author());
    info.committer = convert_git_user(commit.committer());
    info.subject = commit.summary().unwrap_or_default().into();
    info.body = commit.body().unwrap_or_default().into();
    Ok(info)
}
